package cn.jaye.fabric.application;

import cn.jaye.fabric.application.command.chaincode.*;
import cn.jaye.fabric.common.config.ConfigHelper;
import cn.jaye.fabric.common.constants.TestConfig;
import cn.jaye.fabric.common.constants.exception.OrgExceptionEnum;
import cn.jaye.fabric.common.constants.exception.UserExceptionEnum;
import cn.jaye.fabric.common.model.ServiceException;
import cn.jaye.fabric.common.model.SystemSession;
import cn.jaye.fabric.common.util.KeyIDUtil;
import cn.jaye.fabric.domain.model.*;
import cn.jaye.fabric.domain.model.User;
import cn.jaye.fabric.domain.repository.BlockRepository;
import cn.jaye.fabric.domain.repository.ChainCodeRepository;
import cn.jaye.fabric.domain.repository.RelChannelPeerRepository;
import cn.jaye.fabric.domain.repository.TransactionRepository;
import cn.jaye.fabric.domain.service.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.codec.binary.Hex;
import org.hyperledger.fabric.protos.ledger.rwset.kvrwset.KvRwset;
import org.hyperledger.fabric.sdk.*;
import org.hyperledger.fabric.sdk.exception.InvalidArgumentException;
import org.hyperledger.fabric.sdk.exception.TransactionEventException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import static java.lang.String.format;
import static org.hyperledger.fabric.sdk.BlockInfo.EnvelopeType.TRANSACTION_ENVELOPE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * @author Jaye
 * @create 2018-05-02 11:52
 */
@Service
public class ChainCodeApplicationService {

    private Logger logger = LoggerFactory.getLogger(ChainCodeApplicationService.class);

    @Autowired
    private ChannelSerivce channelSerivce;

    @Autowired
    private UserService userService;

    @Autowired
    private TestConfig configFactroy;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private HFClient hfClient;

    @Autowired
    private ChainCodeService chainCodeService;

    @Autowired
    private BlockRepository blockRepository;

    @Autowired
    private TransactionRepository transactionRepository;

    @Autowired
    private OrgService orgService;

    @Autowired
    private RelChannelPeerRepository relChannelPeerRepository;

    @Autowired
    private ChainCodeRepository chainCodeRepository;


    private final ConfigHelper configHelper = new ConfigHelper();

    @Getter
    private Org org;

    @PostConstruct
    private void init() throws NoSuchFieldException, IllegalAccessException {
        logger.info("start initing......");
        configHelper.clearConfig();
        configHelper.customizeConfig();
    }

    public void install(InstallChainCodeCommand installChainCodeCommand) throws Exception {
        User user = SystemSession.getUser();
//        if (!userService.isAdmin(user.getUsername())) {
//            throw new ServiceException(UserExceptionEnum.USER_ADMIN);
//        }
        checkOrg(user.getOrgId());
        MChannel mChannel = new MChannel(installChainCodeCommand.getChannel().getName(), installChainCodeCommand.getChannel().getTxPath());
        Channel channel = hfClient.getChannel(mChannel.getChannelName());
        if (channel == null) {
            channel = channelSerivce.constructChannel(mChannel, getOrg(), installChainCodeCommand.getPeerIds());
        }
        ChainCodeCommand chainCodeCommand = installChainCodeCommand.getChainCode();
        ChainCode chainCode = new ChainCode(chainCodeCommand.getName(), chainCodeCommand.getVersion(), chainCodeCommand.getPath());
        chainCodeService.install(channel, chainCode);

    }

    public void instantiate(InstantiateChainCodeCommand instantiateChainCodeCommand) throws Exception {
        User user = SystemSession.getUser();
//        if (!userService.isAdmin(user.getUsername())) {
//            throw new ServiceException(UserExceptionEnum.USER_ADMIN);
//        }

        checkOrg(user.getOrgId());
        SampleUser peerAdmin = userService.getSampleAdmin(org);
        hfClient.setUserContext(peerAdmin);
        Channel channel = channelSerivce.getChannel(instantiateChainCodeCommand.getChannel().getName(), getOrg(), instantiateChainCodeCommand.getPeerIds());
        ChainCodeCommand chainCodeCommand = instantiateChainCodeCommand.getChainCode();
        ChainCode chainCode = new ChainCode(chainCodeCommand.getName(),
                chainCodeCommand.getVersion(), chainCodeCommand.getPath(), null, null, chainCodeCommand.getArgs());
        chainCodeService.instantiate(channel, chainCode).thenApply(transactionEvent -> {
            createChannelPeerReltion(instantiateChainCodeCommand.getChannel().getName(), instantiateChainCodeCommand.getPeerIds());
            createChainCode(instantiateChainCodeCommand.getChannel().getName(), chainCode);
            return null;
        }).exceptionally(e -> {
            if (e instanceof TransactionEventException) {
                BlockEvent.TransactionEvent te = ((TransactionEventException) e).getTransactionEvent();
                if (te != null) {
                    fail(format("Transaction with txid %s failed. %s", te.getTransactionID(), e.getMessage()));
                }
            }
            return null;
        });
    }

    @Transactional(rollbackFor = Exception.class)
    private void createChannelPeerReltion(String channelName, String[] peerIds) {
        List<RelChannelPeer> relChannelPeers = new ArrayList<>();
        for (String peerId : peerIds) {
            RelChannelPeer channelPeer = new RelChannelPeer(channelName, peerId);
            relChannelPeers.add(channelPeer);
        }
        if (relChannelPeers.size() > 0) {
            relChannelPeerRepository.insertBatch(relChannelPeers);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    private void createChainCode(String channelName, ChainCode chainCode) {
        MChainCode mChainCode = new MChainCode(KeyIDUtil.getUUID(), chainCode.getName(),
                chainCode.getVersion(),
                chainCode.getPath(),
                channelName, 0);
        chainCodeRepository.insert(mChainCode);
    }

    public String invoke(InvokeChainCodeCommand invokeChainCodeCommand) throws Exception {
        User user = SystemSession.getUser();
        checkOrg(user.getOrgId());
        checkUser(user);
        Channel channel = channelSerivce.getChannelByRelPeer(invokeChainCodeCommand.getChannel().getName(), getOrg());
        ChainCodeCommand chainCodeCommand = invokeChainCodeCommand.getChainCode();
        ChainCode chainCode = new ChainCode(chainCodeCommand.getName(),
                chainCodeCommand.getVersion(),
                chainCodeCommand.getPath(),
                chainCodeCommand.getLocation(),
                chainCodeCommand.getFcn(),
                chainCodeCommand.getArgs());
        return chainCodeService.invoke(channel, chainCode);
//                .thenApply(transactionEvent -> {
//            MTransaction mt = new MTransaction();
//            mt.setChannelName(transactionEvent.getChannelId());
//            mt.setTxId(transactionEvent.getTransactionID());
//            mt.setCreateTime(transactionEvent.getTimestamp());
//            createTransaction(mt);
//           return null;
//        });
//        channel.queryBlockByTransactionID(txid);
    }

    @Transactional(rollbackFor = Exception.class)
    private void createTransaction(MTransaction mTransaction) {
        transactionRepository.insert(mTransaction);
    }

    private void checkUser(User user) {
        try {
            hfClient.setUserContext(userService.getUser(user));
        } catch (InvalidArgumentException e) {
            e.printStackTrace();
            throw new ServiceException(UserExceptionEnum.USER_ERROR);
        }
    }

    private void checkOrg(String orgId) {
        Org org = orgService.getOrg(orgId);
        if (org == null) {
            throw new ServiceException(OrgExceptionEnum.NO_DATA);
        }
        this.org = org;
    }

    public String query(QueryChainCodeCommand queryChainCodeCommand) throws Exception {
        User user = SystemSession.getUser();
        checkOrg(user.getOrgId());
        checkUser(user);
        Channel channel = channelSerivce.getChannelByRelPeer(queryChainCodeCommand.getChannel().getName(), getOrg());
        ChainCodeCommand chainCodeCommand = queryChainCodeCommand.getChainCode();
        ChainCode chainCode = new ChainCode(chainCodeCommand.getName(),
                chainCodeCommand.getVersion(),
                chainCodeCommand.getPath(),
                chainCodeCommand.getLocation(),
                chainCodeCommand.getFcn(),
                chainCodeCommand.getArgs());
        return chainCodeService.query(channel, chainCode);
    }

    public String test(QueryChainCodeCommand queryChainCodeCommand) throws Exception {
        User user = SystemSession.getUser();
        checkOrg(user.getOrgId());
        checkUser(user);
        Channel channel = channelSerivce.getChannel(queryChainCodeCommand.getChannel().getName(), getOrg());
//        BlockchainInfo channelInfo = channel.queryBlockchainInfo();
//        BlockInfo returnedBlock = channel.queryBlockByNumber(channelInfo.getHeight() - 1);
//        TransactionContext transactionContext = new TransactionContext(channel, hfClient.getUserContext(), hfClient.getCryptoSuite());
//        String txId = transactionContext.getTxID();
//        TransactionInfo transactionInfo = channel.queryTransactionByID(txId);
        blockWalker(channel);
        return null;
    }

    private String blockWalker(Channel channel) throws Exception {
//        channel.getPeers();
        BlockchainInfo channelInfo = channel.queryBlockchainInfo();
//        channel.q
        for (long current = channelInfo.getHeight() - 1; current > -1; --current) {
            BlockInfo returnedBlock = channel.queryBlockByNumber(current);
            final long blockNumber = returnedBlock.getBlockNumber();
            System.out.println("---------------------------");
            logger.error("current block number {} has data hash: {}", blockNumber, Hex.encodeHexString(returnedBlock.getDataHash()));
            logger.error("current block number {} has previous hash id: {}", blockNumber, Hex.encodeHexString(returnedBlock.getPreviousHash()));
            logger.error("current block number {} has calculated block hash is {}", blockNumber, Hex.encodeHexString(SDKUtils.calculateBlockHash(blockNumber, returnedBlock.getPreviousHash(), returnedBlock.getDataHash())));
            System.out.println("---------------------------");
            final int envelopCount = returnedBlock.getEnvelopCount();
            assertEquals(1, envelopCount);
            int i = 0;
            MBlock mBlock = new MBlock();
            mBlock.setBlockNum(blockNumber);
            mBlock.setDataHash(Hex.encodeHexString(returnedBlock.getDataHash()));
            mBlock.setPreHash(Hex.encodeHexString(returnedBlock.getPreviousHash()));
            mBlock.setTxCount(envelopCount);
            mBlock.setChannelName(channel.getName());
            blockRepository.insert(mBlock);
            for (BlockInfo.EnvelopeInfo envelopeInfo : returnedBlock.getEnvelopeInfos()) {
                ++i;
                logger.error("  Transaction number {} has transaction id: {}", i, envelopeInfo.getTransactionID());
                final String channelId = envelopeInfo.getChannelId();
                logger.error("  Transaction number {} has channel id: {}", i, channelId);
                logger.error("  Transaction number {} has epoch: {}", i, envelopeInfo.getEpoch());
                logger.error("  Transaction number {} has transaction timestamp: {}", i, envelopeInfo.getTimestamp());
                logger.error("  Transaction number {} has type id: {}", i, "" + envelopeInfo.getType());


                MTransaction mTransaction = new MTransaction();
                mTransaction.setTxId(envelopeInfo.getTransactionID());
                mTransaction.setChannelName(channelId);
                mTransaction.setCreateTime(new Date());
                transactionRepository.insert(mTransaction);
                if (envelopeInfo.getType() == TRANSACTION_ENVELOPE) {
                    BlockInfo.TransactionEnvelopeInfo transactionEnvelopeInfo = (BlockInfo.TransactionEnvelopeInfo) envelopeInfo;
                    logger.error("  Transaction number {} has {} actions", i, transactionEnvelopeInfo.getTransactionActionInfoCount());
                    assertEquals(1, transactionEnvelopeInfo.getTransactionActionInfoCount());
                    logger.error("  Transaction number {} isValid {}", i, transactionEnvelopeInfo.isValid());
                    assertEquals(transactionEnvelopeInfo.isValid(), true);
                    logger.error("  Transaction number {} validation code {}", i, transactionEnvelopeInfo.getValidationCode());
                    assertEquals(0, transactionEnvelopeInfo.getValidationCode());
                    int j = 0;
                    for (BlockInfo.TransactionEnvelopeInfo.TransactionActionInfo transactionActionInfo : transactionEnvelopeInfo.getTransactionActionInfos()) {
                        ++j;
                        logger.error("   Transaction action {} has response status {}", j, transactionActionInfo.getResponseStatus());
                        assertEquals(200, transactionActionInfo.getResponseStatus());
                        logger.error("   Transaction action {} has response message bytes as string: {}", j,
                                printableString(new String(transactionActionInfo.getResponseMessageBytes(), "UTF-8")));
                        logger.error("   Transaction action {} has {} endorsements", j, transactionActionInfo.getEndorsementsCount());
                        assertEquals(2, transactionActionInfo.getEndorsementsCount());

                        for (int n = 0; n < transactionActionInfo.getEndorsementsCount(); ++n) {
                            BlockInfo.EndorserInfo endorserInfo = transactionActionInfo.getEndorsementInfo(n);
                            logger.error("Endorser {} signature: {}", n, Hex.encodeHexString(endorserInfo.getSignature()));
                            logger.error("Endorser {} endorser: {}", n, new String(endorserInfo.getEndorser(), "UTF-8"));
                        }
                        logger.error("   Transaction action {} has {} chaincode input arguments", j, transactionActionInfo.getChaincodeInputArgsCount());
                        for (int z = 0; z < transactionActionInfo.getChaincodeInputArgsCount(); ++z) {
                            logger.error("     Transaction action {} has chaincode input argument {} is: {}", j, z,
                                    printableString(new String(transactionActionInfo.getChaincodeInputArgs(z), "UTF-8")));
                        }
                        logger.error("   Transaction action {} proposal response status: {}", j,
                                transactionActionInfo.getProposalResponseStatus());
                        logger.error("   Transaction action {} proposal response payload: {}", j,
                                printableString(new String(transactionActionInfo.getProposalResponsePayload())));

                        TxReadWriteSetInfo rwsetInfo = transactionActionInfo.getTxReadWriteSet();
                        if (null != rwsetInfo) {
                            logger.error("   Transaction action {} has {} name space read write sets", j, rwsetInfo.getNsRwsetCount());

                            for (TxReadWriteSetInfo.NsRwsetInfo nsRwsetInfo : rwsetInfo.getNsRwsetInfos()) {
                                final String namespace = nsRwsetInfo.getNaamespace();
                                KvRwset.KVRWSet rws = nsRwsetInfo.getRwset();

                                int rs = -1;
                                for (KvRwset.KVRead readList : rws.getReadsList()) {
                                    rs++;

                                    logger.error("     Namespace {} read set {} key {}  version [{}:{}]", namespace, rs, readList.getKey(),
                                            readList.getVersion().getBlockNum(), readList.getVersion().getTxNum());

                                }

                                rs = -1;
                                for (KvRwset.KVWrite writeList : rws.getWritesList()) {
                                    rs++;
                                    String valAsString = printableString(new String(writeList.getValue().toByteArray(), "UTF-8"));

                                    logger.error("     Namespace {} write set {} key {} has value '{}' ", namespace, rs,
                                            writeList.getKey(),
                                            valAsString);

                                }
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    static String printableString(final String string) {
        int maxLogStringLength = 64;
        if (string == null || string.length() == 0) {
            return string;
        }

        String ret = string.replaceAll("[^\\p{Print}]", "?");

        ret = ret.substring(0, Math.min(ret.length(), maxLogStringLength)) + (ret.length() > maxLogStringLength ? "..." : "");

        return ret;

    }
}
